#!/usr/bin/env python3

# http://www.drdobbs.com/testing/unit-testing-with-python/240165163

import difflib
import glob
import inspect
import logging
import optparse
import os
import re
import shutil
import sys
import tempfile
import unittest
import textwrap
import yaml
import gzip
from zipfile import ZipFile
from unittest import mock


localmodule = os.path.realpath(
    os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..'))
print('localmodule: ' + localmodule)
if localmodule not in sys.path:
    sys.path.insert(0, localmodule)

import fdroidserver.index
import fdroidserver.signindex
import fdroidserver.common
import fdroidserver.metadata
from testcommon import TmpCwd
from fdroidserver.exception import FDroidException


class CommonTest(unittest.TestCase):
    '''fdroidserver/common.py'''

    def setUp(self):
        logging.basicConfig(level=logging.DEBUG)
        self.basedir = os.path.join(localmodule, 'tests')
        self.tmpdir = os.path.abspath(os.path.join(self.basedir, '..', '.testfiles'))
        if not os.path.exists(self.tmpdir):
            os.makedirs(self.tmpdir)
        os.chdir(self.basedir)

    def test_assert_config_keystore(self):
        with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir):
            with self.assertRaises(FDroidException):
                fdroidserver.common.assert_config_keystore({})

        with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir):
            c = {'repo_keyalias': 'localhost',
                 'keystore': 'keystore.jks',
                 'keystorepass': '12345',
                 'keypass': '12345'}
            with open('keystore.jks', 'w'):
                pass
            fdroidserver.common.assert_config_keystore(c)

    def _set_build_tools(self):
        build_tools = os.path.join(fdroidserver.common.config['sdk_path'], 'build-tools')
        if os.path.exists(build_tools):
            fdroidserver.common.config['build_tools'] = ''
            for f in sorted(os.listdir(build_tools), reverse=True):
                versioned = os.path.join(build_tools, f)
                if os.path.isdir(versioned) \
                        and os.path.isfile(os.path.join(versioned, 'aapt')):
                    fdroidserver.common.config['build_tools'] = versioned
                    break
            return True
        else:
            print('no build-tools found: ' + build_tools)
            return False

    def _find_all(self):
        tools = ['aapt', 'adb', 'zipalign']
        if os.path.exists(os.path.join(os.getenv('ANDROID_HOME'), 'tools', 'android')):
            tools.append('android')
        for cmd in tools:
            path = fdroidserver.common.find_sdk_tools_cmd(cmd)
            if path is not None:
                self.assertTrue(os.path.exists(path))
                self.assertTrue(os.path.isfile(path))

    def test_find_sdk_tools_cmd(self):
        fdroidserver.common.config = dict()
        # TODO add this once everything works without sdk_path set in config
        # self._find_all()
        sdk_path = os.getenv('ANDROID_HOME')
        if os.path.exists(sdk_path):
            fdroidserver.common.config['sdk_path'] = sdk_path
            build_tools = os.path.join(sdk_path, 'build-tools')
            if self._set_build_tools() or os.path.exists('/usr/bin/aapt'):
                self._find_all()
            else:
                print('no build-tools found: ' + build_tools)

    def test_find_java_root_path(self):
        testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir)
        os.chdir(testdir)

        all_pathlists = [
            ([  # Debian
                '/usr/lib/jvm/java-1.5.0-gcj-5-amd64',
                '/usr/lib/jvm/java-8-openjdk-amd64',
                '/usr/lib/jvm/java-1.8.0-openjdk-amd64',
            ], '/usr/lib/jvm/java-8-openjdk-amd64'),
            ([  # OSX
                '/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk',
                '/Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk',
                '/System/Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk',
            ], '/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk'),
        ]

        for pathlist, choice in all_pathlists:
            # strip leading / to make relative paths to test without root
            pathlist = [p[1:] for p in pathlist]

            # create test file used in common._add_java_paths_to_config()
            for p in pathlist:
                if p.startswith('/System') or p.startswith('/Library'):
                    basedir = os.path.join(p, 'Contents', 'Home', 'bin')
                else:
                    basedir = os.path.join(p, 'bin')
                os.makedirs(basedir)
                open(os.path.join(basedir, 'javac'), 'w').close()

            config = dict()
            config['java_paths'] = dict()
            fdroidserver.common._add_java_paths_to_config(pathlist, config)
            self.assertEqual(config['java_paths']['8'], choice[1:])

    def test_is_apk_and_debuggable(self):
        config = dict()
        fdroidserver.common.fill_config_defaults(config)
        fdroidserver.common.config = config
        self._set_build_tools()
        try:
            config['aapt'] = fdroidserver.common.find_sdk_tools_cmd('aapt')
        except fdroidserver.exception.FDroidException:
            pass  # aapt is not required if androguard is present

        # these are set debuggable
        testfiles = []
        testfiles.append(os.path.join(self.basedir, 'urzip.apk'))
        testfiles.append(os.path.join(self.basedir, 'urzip-badsig.apk'))
        testfiles.append(os.path.join(self.basedir, 'urzip-badcert.apk'))
        for apkfile in testfiles:
            debuggable = fdroidserver.common.is_apk_and_debuggable(apkfile)
            self.assertTrue(debuggable,
                            "debuggable APK state was not properly parsed!")
            if 'aapt' in config:
                self.assertTrue(fdroidserver.common.is_apk_and_debuggable_aapt(apkfile),
                                'aapt parsing missed <application android:debuggable="">!')
            if fdroidserver.common.use_androguard():
                self.assertTrue(fdroidserver.common.is_apk_and_debuggable_androguard(apkfile),
                                'androguard missed <application android:debuggable="">!')

        # these are set NOT debuggable
        testfiles = []
        testfiles.append(os.path.join(self.basedir, 'urzip-release.apk'))
        testfiles.append(os.path.join(self.basedir, 'urzip-release-unsigned.apk'))
        testfiles.append(os.path.join(self.basedir, 'v2.only.sig_2.apk'))
        for apkfile in testfiles:
            debuggable = fdroidserver.common.is_apk_and_debuggable(apkfile)
            self.assertFalse(debuggable,
                             "debuggable APK state was not properly parsed!")
            if 'aapt' in config:
                self.assertFalse(fdroidserver.common.is_apk_and_debuggable_aapt(apkfile),
                                 'aapt parsing missed <application android:debuggable="">!')
            if fdroidserver.common.use_androguard():
                self.assertFalse(fdroidserver.common.is_apk_and_debuggable_androguard(apkfile),
                                 'androguard missed <application android:debuggable="">!')

    VALID_STRICT_PACKAGE_NAMES = [
        "An.stop",
        "SpeedoMeterApp.main",
        "a2dp.Vol",
        "au.com.darkside.XServer",
        "click.dummer.UartSmartwatch",
        "com.Bisha.TI89EmuDonation",
        "com.MarcosDiez.shareviahttp",
        "com.Pau.ImapNotes2",
        "com.app.Zensuren",
        "com.darshancomputing.BatteryIndicator",
        "com.geecko.QuickLyric",
        "com.genonbeta.TrebleShot",
        "com.gpl.rpg.AndorsTrail",
        "com.hobbyone.HashDroid",
        "com.moez.QKSMS",
        "com.platypus.SAnd",
        "com.prhlt.aemus.Read4SpeechExperiments",
        "de.syss.MifareClassicTool",
        "org.fdroid.fdroid",
        "org.f_droid.fdr0ID",
    ]

    def test_is_valid_package_name(self):
        for name in self.VALID_STRICT_PACKAGE_NAMES + [
                "_SpeedoMeterApp.main",
                "05041684efd9b16c2888b1eddbadd0359f655f311b89bdd1737f560a10d20fb8"]:
            self.assertTrue(fdroidserver.common.is_valid_package_name(name),
                            "{0} should be a valid package name".format(name))
        for name in ["0rg.fdroid.fdroid",
                     ".f_droid.fdr0ID",
                     "trailingdot.",
                     "org.fdroid/fdroid",
                     "/org.fdroid.fdroid"]:
            self.assertFalse(fdroidserver.common.is_valid_package_name(name),
                             "{0} should not be a valid package name".format(name))

    def test_is_strict_application_id(self):
        """see also tests/valid-package-names/"""
        for name in self.VALID_STRICT_PACKAGE_NAMES:
            self.assertTrue(fdroidserver.common.is_strict_application_id(name),
                            "{0} should be a strict application id".format(name))
        for name in ["0rg.fdroid.fdroid",
                     ".f_droid.fdr0ID",
                     "oneword",
                     "trailingdot.",
                     "cafebabe",
                     "org.fdroid/fdroid",
                     "/org.fdroid.fdroid",
                     "_SpeedoMeterApp.main",
                     "05041684efd9b16c2888b1eddbadd0359f655f311b89bdd1737f560a10d20fb8"]:
            self.assertFalse(fdroidserver.common.is_strict_application_id(name),
                             "{0} should not be a strict application id".format(name))

    def test_prepare_sources(self):
        testint = 99999999
        teststr = 'FAKE_STR_FOR_TESTING'

        testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir)
        shutil.copytree(os.path.join(self.basedir, 'source-files'),
                        os.path.join(testdir, 'source-files'))

        fdroidclient_testdir = os.path.join(testdir, 'source-files', 'fdroid', 'fdroidclient')

        config = dict()
        config['sdk_path'] = os.getenv('ANDROID_HOME')
        config['ndk_paths'] = {'r10d': os.getenv('ANDROID_NDK_HOME')}
        config['build_tools'] = 'FAKE_BUILD_TOOLS_VERSION'
        fdroidserver.common.config = config
        app = fdroidserver.metadata.App()
        app.id = 'org.fdroid.froid'
        build = fdroidserver.metadata.Build()
        build.commit = 'master'
        build.forceversion = True
        build.forcevercode = True
        build.gradle = ['yes']
        build.target = 'android-' + str(testint)
        build.versionName = teststr
        build.versionCode = testint

        class FakeVcs():
            # no need to change to the correct commit here
            def gotorevision(self, rev, refresh=True):
                pass

            # no srclib info needed, but it could be added...
            def getsrclib(self):
                return None

        fdroidserver.common.prepare_source(FakeVcs(), app, build,
                                           fdroidclient_testdir, fdroidclient_testdir, fdroidclient_testdir)

        with open(os.path.join(fdroidclient_testdir, 'build.gradle'), 'r') as f:
            filedata = f.read()
        self.assertIsNotNone(re.search(r"\s+compileSdkVersion %s\s+" % testint, filedata))

        with open(os.path.join(fdroidclient_testdir, 'AndroidManifest.xml')) as f:
            filedata = f.read()
        self.assertIsNone(re.search('android:debuggable', filedata))
        self.assertIsNotNone(re.search('android:versionName="%s"' % build.versionName, filedata))
        self.assertIsNotNone(re.search('android:versionCode="%s"' % build.versionCode, filedata))

    def test_prepare_sources_refresh(self):
        packageName = 'org.fdroid.ci.test.app'
        testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir)
        print('testdir', testdir)
        os.chdir(testdir)
        os.mkdir('build')
        os.mkdir('metadata')

        # use a local copy if available to avoid hitting the network
        tmprepo = os.path.join(self.basedir, 'tmp', 'importer')
        if os.path.exists(tmprepo):
            git_url = tmprepo
        else:
            git_url = 'https://gitlab.com/fdroid/ci-test-app.git'

        metadata = dict()
        metadata['Description'] = 'This is just a test app'
        metadata['RepoType'] = 'git'
        metadata['Repo'] = git_url
        with open(os.path.join('metadata', packageName + '.yml'), 'w') as fp:
            yaml.dump(metadata, fp)

        gitrepo = os.path.join(testdir, 'build', packageName)
        vcs0 = fdroidserver.common.getvcs('git', git_url, gitrepo)
        vcs0.gotorevision('0.3', refresh=True)
        vcs1 = fdroidserver.common.getvcs('git', git_url, gitrepo)
        vcs1.gotorevision('0.3', refresh=False)

    def test_fdroid_popen_stderr_redirect(self):
        config = dict()
        fdroidserver.common.fill_config_defaults(config)
        fdroidserver.common.config = config

        commands = ['sh', '-c', 'echo stdout message && echo stderr message 1>&2']

        p = fdroidserver.common.FDroidPopen(commands)
        self.assertEqual(p.output, 'stdout message\nstderr message\n')

        p = fdroidserver.common.FDroidPopen(commands, stderr_to_stdout=False)
        self.assertEqual(p.output, 'stdout message\n')

    def test_signjar(self):
        fdroidserver.common.config = None
        config = fdroidserver.common.read_config(fdroidserver.common.options)
        config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
        fdroidserver.common.config = config
        fdroidserver.signindex.config = config

        sourcedir = os.path.join(self.basedir, 'signindex')
        testsdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir)
        for f in ('testy.jar', 'guardianproject.jar',):
            sourcefile = os.path.join(sourcedir, f)
            testfile = os.path.join(testsdir, f)
            shutil.copy(sourcefile, testsdir)
            fdroidserver.signindex.sign_jar(testfile)
            # these should be resigned, and therefore different
            self.assertNotEqual(open(sourcefile, 'rb').read(), open(testfile, 'rb').read())

    def test_verify_apk_signature(self):
        fdroidserver.common.config = None
        config = fdroidserver.common.read_config(fdroidserver.common.options)
        config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
        fdroidserver.common.config = config

        self.assertTrue(fdroidserver.common.verify_apk_signature('bad-unicode-πÇÇ现代通用字-български-عربي1.apk'))
        if 'apksigner' in fdroidserver.common.config:  # apksigner considers MD5 signatures valid
            self.assertTrue(fdroidserver.common.verify_apk_signature('org.bitbucket.tickytacky.mirrormirror_1.apk'))
            self.assertTrue(fdroidserver.common.verify_apk_signature('org.bitbucket.tickytacky.mirrormirror_2.apk'))
            self.assertTrue(fdroidserver.common.verify_apk_signature('org.bitbucket.tickytacky.mirrormirror_3.apk'))
            self.assertTrue(fdroidserver.common.verify_apk_signature('org.bitbucket.tickytacky.mirrormirror_4.apk'))
        else:
            self.assertFalse(fdroidserver.common.verify_apk_signature('org.bitbucket.tickytacky.mirrormirror_1.apk'))
            self.assertFalse(fdroidserver.common.verify_apk_signature('org.bitbucket.tickytacky.mirrormirror_2.apk'))
            self.assertFalse(fdroidserver.common.verify_apk_signature('org.bitbucket.tickytacky.mirrormirror_3.apk'))
            self.assertFalse(fdroidserver.common.verify_apk_signature('org.bitbucket.tickytacky.mirrormirror_4.apk'))
        self.assertTrue(fdroidserver.common.verify_apk_signature('org.dyndns.fules.ck_20.apk'))
        self.assertTrue(fdroidserver.common.verify_apk_signature('urzip.apk'))
        self.assertFalse(fdroidserver.common.verify_apk_signature('urzip-badcert.apk'))
        self.assertFalse(fdroidserver.common.verify_apk_signature('urzip-badsig.apk'))
        self.assertTrue(fdroidserver.common.verify_apk_signature('urzip-release.apk'))
        self.assertFalse(fdroidserver.common.verify_apk_signature('urzip-release-unsigned.apk'))

    def test_verify_old_apk_signature(self):
        fdroidserver.common.config = None
        config = fdroidserver.common.read_config(fdroidserver.common.options)
        config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
        fdroidserver.common.config = config

        self.assertTrue(fdroidserver.common.verify_old_apk_signature('bad-unicode-πÇÇ现代通用字-български-عربي1.apk'))
        self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.bitbucket.tickytacky.mirrormirror_1.apk'))
        self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.bitbucket.tickytacky.mirrormirror_2.apk'))
        self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.bitbucket.tickytacky.mirrormirror_3.apk'))
        self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.bitbucket.tickytacky.mirrormirror_4.apk'))
        self.assertTrue(fdroidserver.common.verify_old_apk_signature('org.dyndns.fules.ck_20.apk'))
        self.assertTrue(fdroidserver.common.verify_old_apk_signature('urzip.apk'))
        self.assertFalse(fdroidserver.common.verify_old_apk_signature('urzip-badcert.apk'))
        self.assertFalse(fdroidserver.common.verify_old_apk_signature('urzip-badsig.apk'))
        self.assertTrue(fdroidserver.common.verify_old_apk_signature('urzip-release.apk'))
        self.assertFalse(fdroidserver.common.verify_old_apk_signature('urzip-release-unsigned.apk'))

    def test_verify_jar_signature_succeeds(self):
        fdroidserver.common.config = None
        config = fdroidserver.common.read_config(fdroidserver.common.options)
        config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
        fdroidserver.common.config = config
        source_dir = os.path.join(self.basedir, 'signindex')
        for f in ('testy.jar', 'guardianproject.jar'):
            testfile = os.path.join(source_dir, f)
            fdroidserver.common.verify_jar_signature(testfile)

    def test_verify_jar_signature_fails(self):
        fdroidserver.common.config = None
        config = fdroidserver.common.read_config(fdroidserver.common.options)
        config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
        fdroidserver.common.config = config
        source_dir = os.path.join(self.basedir, 'signindex')
        testfile = os.path.join(source_dir, 'unsigned.jar')
        with self.assertRaises(fdroidserver.index.VerificationException):
            fdroidserver.common.verify_jar_signature(testfile)

    def test_verify_apks(self):
        fdroidserver.common.config = None
        config = fdroidserver.common.read_config(fdroidserver.common.options)
        config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
        fdroidserver.common.config = config

        sourceapk = os.path.join(self.basedir, 'urzip.apk')

        testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir)
        print('testdir', testdir)

        copyapk = os.path.join(testdir, 'urzip-copy.apk')
        shutil.copy(sourceapk, copyapk)
        self.assertTrue(fdroidserver.common.verify_apk_signature(copyapk))
        self.assertIsNone(fdroidserver.common.verify_apks(sourceapk, copyapk, self.tmpdir))

        unsignedapk = os.path.join(testdir, 'urzip-unsigned.apk')
        with ZipFile(sourceapk, 'r') as apk:
            with ZipFile(unsignedapk, 'w') as testapk:
                for info in apk.infolist():
                    if not info.filename.startswith('META-INF/'):
                        testapk.writestr(info, apk.read(info.filename))
        self.assertIsNone(fdroidserver.common.verify_apks(sourceapk, unsignedapk, self.tmpdir))

        twosigapk = os.path.join(testdir, 'urzip-twosig.apk')
        otherapk = ZipFile(os.path.join(self.basedir, 'urzip-release.apk'), 'r')
        with ZipFile(sourceapk, 'r') as apk:
            with ZipFile(twosigapk, 'w') as testapk:
                for info in apk.infolist():
                    testapk.writestr(info, apk.read(info.filename))
                    if info.filename.startswith('META-INF/'):
                        testapk.writestr(info, otherapk.read(info.filename))
        otherapk.close()
        self.assertFalse(fdroidserver.common.verify_apk_signature(twosigapk))
        self.assertIsNone(fdroidserver.common.verify_apks(sourceapk, twosigapk, self.tmpdir))

    def test_write_to_config(self):
        with tempfile.TemporaryDirectory() as tmpPath:
            cfgPath = os.path.join(tmpPath, 'config.py')
            with open(cfgPath, 'w') as f:
                f.write(textwrap.dedent("""\
                    # abc
                    # test = 'example value'
                    default_me= '%%%'

                    # comment
                    do_not_touch = "good value"
                    default_me="!!!"

                    key="123"    # inline"""))

            cfg = {'key': '111', 'default_me_orig': 'orig'}
            fdroidserver.common.write_to_config(cfg, 'key', config_file=cfgPath)
            fdroidserver.common.write_to_config(cfg, 'default_me', config_file=cfgPath)
            fdroidserver.common.write_to_config(cfg, 'test', value='test value', config_file=cfgPath)
            fdroidserver.common.write_to_config(cfg, 'new_key', value='new', config_file=cfgPath)

            with open(cfgPath, 'r') as f:
                self.assertEqual(f.read(), textwrap.dedent("""\
                    # abc
                    test = 'test value'
                    default_me = 'orig'

                    # comment
                    do_not_touch = "good value"

                    key = "111"    # inline

                    new_key = "new"
                    """))

    def test_write_to_config_when_empty(self):
        with tempfile.TemporaryDirectory() as tmpPath:
            cfgPath = os.path.join(tmpPath, 'config.py')
            with open(cfgPath, 'w') as f:
                pass
            fdroidserver.common.write_to_config({}, 'key', 'val', cfgPath)
            with open(cfgPath, 'r') as f:
                self.assertEqual(f.read(), textwrap.dedent("""\

                key = "val"
                """))

    def test_apk_name_regex(self):
        good = [
            'urzipπÇÇπÇÇ现代汉语通用字българскиعربي1234ö_-123456.apk',
            'urzipπÇÇπÇÇ现代汉语通用字българскиعربي1234ö_123456_abcdef0.apk',
            'urzip_-123456.apk',
            'a0_0.apk',
            'Z0_0.apk',
            'a0_0_abcdef0.apk',
            'a_a_a_a_0_abcdef0.apk',
            'a_____0.apk',
            'a_____123456_abcdef0.apk',
            'org.fdroid.fdroid_123456.apk',
            # valid, but "_99999" is part of packageName rather than versionCode
            'org.fdroid.fdroid_99999_123456.apk',
            # should be valid, but I can't figure out the regex since \w includes digits
            # 'πÇÇπÇÇ现代汉语通用字българскиعربي1234ö_0_123bafd.apk',
        ]
        for name in good:
            m = fdroidserver.common.APK_NAME_REGEX.match(name)
            self.assertIsNotNone(m)
            self.assertIn(m.group(2), ('-123456', '0', '123456'))
            self.assertIn(m.group(3), ('abcdef0', None))

        bad = [
            'urzipπÇÇπÇÇ现代汉语通用字българскиعربي1234ö_123456_abcdefg.apk',
            'urzip-_-198274.apk',
            'urzip-_0_123bafd.apk',
            'no spaces allowed_123.apk',
            '0_0.apk',
            '0_0_abcdef0.apk',
        ]
        for name in bad:
            self.assertIsNone(fdroidserver.common.APK_NAME_REGEX.match(name))

    def test_standard_file_name_regex(self):
        good = [
            'urzipπÇÇπÇÇ现代汉语通用字българскиعربي1234ö_-123456.mp3',
            'urzipπÇÇπÇÇ现代汉语通用字българскиعربي1234ö_123456.mov',
            'Document_-123456.pdf',
            'WTF_0.MOV',
            'Z0_0.ebk',
            'a_a_a_a_0.txt',
            'org.fdroid.fdroid.privileged.ota_123456.zip',
            'πÇÇπÇÇ现代汉语通用字българскиعربي1234ö_0.jpeg',
            'a_____0.PNG',
            # valid, but "_99999" is part of packageName rather than versionCode
            'a_____99999_123456.zip',
            'org.fdroid.fdroid_99999_123456.zip',
        ]
        for name in good:
            m = fdroidserver.common.STANDARD_FILE_NAME_REGEX.match(name)
            self.assertIsNotNone(m)
            self.assertIn(m.group(2), ('-123456', '0', '123456'))

        bad = [
            'urzipπÇÇπÇÇ现代汉语通用字българскиعربي1234ö_abcdefg.JPEG',
            'urzip-_-198274.zip',
            'urzip-_123bafd.pdf',
            'no spaces allowed_123.foobar',
            'a_____0.',
        ]
        for name in bad:
            self.assertIsNone(fdroidserver.common.STANDARD_FILE_NAME_REGEX.match(name))

    def test_apk_signer_fingerprint(self):

        # fingerprints fetched with: keytool -printcert -file ____.RSA
        testapks = (('repo/obb.main.oldversion_1444412523.apk',
                     '818e469465f96b704e27be2fee4c63ab9f83ddf30e7a34c7371a4728d83b0bc1'),
                    ('repo/obb.main.twoversions_1101613.apk',
                     '32a23624c201b949f085996ba5ed53d40f703aca4989476949cae891022e0ed6'),
                    ('repo/obb.main.twoversions_1101617.apk',
                     '32a23624c201b949f085996ba5ed53d40f703aca4989476949cae891022e0ed6'))

        for apkfile, keytoolcertfingerprint in testapks:
            self.assertEqual(keytoolcertfingerprint,
                             fdroidserver.common.apk_signer_fingerprint(apkfile))

    def test_apk_signer_fingerprint_short(self):

        # fingerprints fetched with: keytool -printcert -file ____.RSA
        testapks = (('repo/obb.main.oldversion_1444412523.apk', '818e469'),
                    ('repo/obb.main.twoversions_1101613.apk', '32a2362'),
                    ('repo/obb.main.twoversions_1101617.apk', '32a2362'))

        for apkfile, keytoolcertfingerprint in testapks:
            self.assertEqual(keytoolcertfingerprint,
                             fdroidserver.common.apk_signer_fingerprint_short(apkfile))

    def test_sign_apk(self):
        try:
            fdroidserver.common.find_sdk_tools_cmd('aapt')
            fdroidserver.common.find_sdk_tools_cmd('zipalign')
        except fdroidserver.exception.FDroidException:
            print('\n\nSKIPPING test_sign_apk, zipalign is not installed!\n')
            return

        fdroidserver.common.config = None
        config = fdroidserver.common.read_config(fdroidserver.common.options)
        config['jarsigner'] = fdroidserver.common.find_sdk_tools_cmd('jarsigner')
        config['keyalias'] = 'sova'
        config['keystorepass'] = 'r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI='
        config['keypass'] = 'r9aquRHYoI8+dYz6jKrLntQ5/NJNASFBacJh7Jv2BlI='
        config['keystore'] = os.path.join(self.basedir, 'keystore.jks')
        fdroidserver.common.config = config
        fdroidserver.signindex.config = config

        testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir)
        unsigned = os.path.join(testdir, 'urzip-release-unsigned.apk')
        signed = os.path.join(testdir, 'urzip-release.apk')

        self.assertFalse(fdroidserver.common.verify_apk_signature(unsigned))

        shutil.copy(os.path.join(self.basedir, 'urzip-release-unsigned.apk'), testdir)
        fdroidserver.common.sign_apk(unsigned, signed, config['keyalias'])
        self.assertTrue(os.path.isfile(signed))
        self.assertFalse(os.path.isfile(unsigned))
        self.assertTrue(fdroidserver.common.verify_apk_signature(signed))

        # now sign an APK with minSdkVersion >= 18
        unsigned = os.path.join(testdir, 'duplicate.permisssions_9999999-unsigned.apk')
        signed = os.path.join(testdir, 'duplicate.permisssions_9999999.apk')
        shutil.copy(os.path.join(self.basedir, 'repo', 'duplicate.permisssions_9999999.apk'),
                    os.path.join(unsigned))
        fdroidserver.common.apk_strip_signatures(unsigned, strip_manifest=True)
        fdroidserver.common.sign_apk(unsigned, signed, config['keyalias'])
        self.assertTrue(os.path.isfile(signed))
        self.assertFalse(os.path.isfile(unsigned))
        self.assertTrue(fdroidserver.common.verify_apk_signature(signed))
        try:
            fdroidserver.common.find_sdk_tools_cmd('aapt')
            self.assertEqual(18, fdroidserver.common.get_minSdkVersion_aapt(signed))
        except fdroidserver.exception.FDroidException:
            print('\n\nSKIPPING test_sign_apk min SDK check, aapt is not installed!\n')
            return

    def test_get_apk_id(self):

        config = dict()
        fdroidserver.common.fill_config_defaults(config)
        fdroidserver.common.config = config
        self._set_build_tools()
        try:
            config['aapt'] = fdroidserver.common.find_sdk_tools_cmd('aapt')
        except fdroidserver.exception.FDroidException:
            pass  # aapt is not required if androguard is present

        testcases = [
            ('repo/obb.main.twoversions_1101613.apk', 'obb.main.twoversions', '1101613', '0.1'),
            ('org.bitbucket.tickytacky.mirrormirror_1.apk', 'org.bitbucket.tickytacky.mirrormirror', '1', '1.0'),
            ('org.bitbucket.tickytacky.mirrormirror_2.apk', 'org.bitbucket.tickytacky.mirrormirror', '2', '1.0.1'),
            ('org.bitbucket.tickytacky.mirrormirror_3.apk', 'org.bitbucket.tickytacky.mirrormirror', '3', '1.0.2'),
            ('org.bitbucket.tickytacky.mirrormirror_4.apk', 'org.bitbucket.tickytacky.mirrormirror', '4', '1.0.3'),
            ('org.dyndns.fules.ck_20.apk', 'org.dyndns.fules.ck', '20', 'v1.6pre2'),
            ('urzip.apk', 'info.guardianproject.urzip', '100', '0.1'),
            ('urzip-badcert.apk', 'info.guardianproject.urzip', '100', '0.1'),
            ('urzip-badsig.apk', 'info.guardianproject.urzip', '100', '0.1'),
            ('urzip-release.apk', 'info.guardianproject.urzip', '100', '0.1'),
            ('urzip-release-unsigned.apk', 'info.guardianproject.urzip', '100', '0.1'),
            ('repo/com.politedroid_3.apk', 'com.politedroid', '3', '1.2'),
            ('repo/com.politedroid_4.apk', 'com.politedroid', '4', '1.3'),
            ('repo/com.politedroid_5.apk', 'com.politedroid', '5', '1.4'),
            ('repo/com.politedroid_6.apk', 'com.politedroid', '6', '1.5'),
            ('repo/duplicate.permisssions_9999999.apk', 'duplicate.permisssions', '9999999', ''),
            ('repo/info.zwanenburg.caffeinetile_4.apk', 'info.zwanenburg.caffeinetile', '4', '1.3'),
            ('repo/obb.main.oldversion_1444412523.apk', 'obb.main.oldversion', '1444412523', '0.1'),
            ('repo/obb.mainpatch.current_1619_another-release-key.apk', 'obb.mainpatch.current', '1619', '0.1'),
            ('repo/obb.mainpatch.current_1619.apk', 'obb.mainpatch.current', '1619', '0.1'),
            ('repo/obb.main.twoversions_1101613.apk', 'obb.main.twoversions', '1101613', '0.1'),
            ('repo/obb.main.twoversions_1101615.apk', 'obb.main.twoversions', '1101615', '0.1'),
            ('repo/obb.main.twoversions_1101617.apk', 'obb.main.twoversions', '1101617', '0.1'),
            ('repo/urzip-; Рахма́, [rɐxˈmanʲɪnəf] سيرجي_رخمانينوف 谢·.apk', 'info.guardianproject.urzip', '100', '0.1'),
        ]
        for apkfilename, appid, versionCode, versionName in testcases:
            if 'aapt' in config:
                a, vc, vn = fdroidserver.common.get_apk_id_aapt(apkfilename)
                self.assertEqual(appid, a, 'aapt appid parsing failed for ' + apkfilename)
                self.assertEqual(versionCode, vc, 'aapt versionCode parsing failed for ' + apkfilename)
                self.assertEqual(versionName, vn, 'aapt versionName parsing failed for ' + apkfilename)
            if fdroidserver.common.use_androguard():
                a, vc, vn = fdroidserver.common.get_apk_id(apkfilename)
                self.assertEqual(appid, a, 'androguard appid parsing failed for ' + apkfilename)
                self.assertEqual(versionName, vn, 'androguard versionName parsing failed for ' + apkfilename)
                self.assertEqual(versionCode, vc, 'androguard versionCode parsing failed for ' + apkfilename)

        with self.assertRaises(FDroidException):
            fdroidserver.common.get_apk_id('nope')

    def test_get_apk_id_aapt_regex(self):
        files = glob.glob(os.path.join(self.basedir, 'build-tools', '[1-9]*.*', '*.txt'))
        self.assertNotEqual(0, len(files))
        for f in files:
            appid, versionCode = os.path.splitext(os.path.basename(f))[0][12:].split('_')
            with open(f) as fp:
                m = fdroidserver.common.APK_ID_TRIPLET_REGEX.match(fp.read())
                if m:
                    self.assertEqual(appid, m.group(1))
                    self.assertEqual(versionCode, m.group(2))
                else:
                    self.fail('could not parse aapt output: {}'.format(f))

    def test_get_native_code(self):
        testcases = [
            ('repo/obb.main.twoversions_1101613.apk', []),
            ('org.bitbucket.tickytacky.mirrormirror_1.apk', []),
            ('org.bitbucket.tickytacky.mirrormirror_2.apk', []),
            ('org.bitbucket.tickytacky.mirrormirror_3.apk', []),
            ('org.bitbucket.tickytacky.mirrormirror_4.apk', []),
            ('org.dyndns.fules.ck_20.apk', ['arm64-v8a', 'armeabi', 'armeabi-v7a', 'mips', 'mips64', 'x86', 'x86_64']),
            ('urzip.apk', []),
            ('urzip-badcert.apk', []),
            ('urzip-badsig.apk', []),
            ('urzip-release.apk', []),
            ('urzip-release-unsigned.apk', []),
            ('repo/com.politedroid_3.apk', []),
            ('repo/com.politedroid_4.apk', []),
            ('repo/com.politedroid_5.apk', []),
            ('repo/com.politedroid_6.apk', []),
            ('repo/duplicate.permisssions_9999999.apk', []),
            ('repo/info.zwanenburg.caffeinetile_4.apk', []),
            ('repo/obb.main.oldversion_1444412523.apk', []),
            ('repo/obb.mainpatch.current_1619_another-release-key.apk', []),
            ('repo/obb.mainpatch.current_1619.apk', []),
            ('repo/obb.main.twoversions_1101613.apk', []),
            ('repo/obb.main.twoversions_1101615.apk', []),
            ('repo/obb.main.twoversions_1101617.apk', []),
            ('repo/urzip-; Рахма́, [rɐxˈmanʲɪnəf] سيرجي_رخمانينوف 谢·.apk', []),
        ]
        for apkfilename, native_code in testcases:
            nc = fdroidserver.common.get_native_code(apkfilename)
            self.assertEqual(native_code, nc)

    def test_get_minSdkVersion_aapt(self):
        config = dict()
        fdroidserver.common.fill_config_defaults(config)
        fdroidserver.common.config = config
        self._set_build_tools()
        try:  # get_minSdkVersion_aapt requires aapt
            config['aapt'] = fdroidserver.common.find_sdk_tools_cmd('aapt')
        except fdroidserver.exception.FDroidException:
            print('\n\nSKIPPING test_sign_apk, aapt is not installed!\n')
            return

        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('bad-unicode-πÇÇ现代通用字-български-عربي1.apk')
        self.assertEqual(4, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.bitbucket.tickytacky.mirrormirror_1.apk')
        self.assertEqual(14, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.bitbucket.tickytacky.mirrormirror_2.apk')
        self.assertEqual(14, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.bitbucket.tickytacky.mirrormirror_3.apk')
        self.assertEqual(14, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.bitbucket.tickytacky.mirrormirror_4.apk')
        self.assertEqual(14, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('org.dyndns.fules.ck_20.apk')
        self.assertEqual(7, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip.apk')
        self.assertEqual(4, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip-badcert.apk')
        self.assertEqual(4, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip-badsig.apk')
        self.assertEqual(4, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip-release.apk')
        self.assertEqual(4, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('urzip-release-unsigned.apk')
        self.assertEqual(4, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/com.politedroid_3.apk')
        self.assertEqual(3, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/com.politedroid_4.apk')
        self.assertEqual(3, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/com.politedroid_5.apk')
        self.assertEqual(3, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/com.politedroid_6.apk')
        self.assertEqual(14, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.main.oldversion_1444412523.apk')
        self.assertEqual(4, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.mainpatch.current_1619_another-release-key.apk')
        self.assertEqual(4, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.mainpatch.current_1619.apk')
        self.assertEqual(4, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.main.twoversions_1101613.apk')
        self.assertEqual(4, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.main.twoversions_1101615.apk')
        self.assertEqual(4, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/obb.main.twoversions_1101617.apk')
        self.assertEqual(4, minSdkVersion)
        minSdkVersion = fdroidserver.common.get_minSdkVersion_aapt('repo/urzip-; Рахма́, [rɐxˈmanʲɪnəf] سيرجي_رخمانينوف 谢·.apk')

        with self.assertRaises(FDroidException):
            fdroidserver.common.get_minSdkVersion_aapt('nope')

    def test_apk_release_name(self):
        appid, vercode, sigfp = fdroidserver.common.apk_parse_release_filename('com.serwylo.lexica_905.apk')
        self.assertEqual(appid, 'com.serwylo.lexica')
        self.assertEqual(vercode, '905')
        self.assertEqual(sigfp, None)

        appid, vercode, sigfp = fdroidserver.common.apk_parse_release_filename('com.serwylo.lexica_905_c82e0f6.apk')
        self.assertEqual(appid, 'com.serwylo.lexica')
        self.assertEqual(vercode, '905')
        self.assertEqual(sigfp, 'c82e0f6')

        appid, vercode, sigfp = fdroidserver.common.apk_parse_release_filename('beverly_hills-90210.apk')
        self.assertEqual(appid, None)
        self.assertEqual(vercode, None)
        self.assertEqual(sigfp, None)

    def test_metadata_find_developer_signature(self):
        sig = fdroidserver.common.metadata_find_developer_signature('org.smssecure.smssecure')
        self.assertEqual('b30bb971af0d134866e158ec748fcd553df97c150f58b0a963190bbafbeb0868', sig)

    def test_parse_xml(self):
        manifest = os.path.join('source-files', 'fdroid', 'fdroidclient', 'AndroidManifest.xml')
        parsed = fdroidserver.common.parse_xml(manifest)
        self.assertIsNotNone(parsed)
        self.assertEqual(str(type(parsed)), "<class 'xml.etree.ElementTree.Element'>")

    def test_parse_androidmanifests(self):
        app = fdroidserver.metadata.App()
        app.id = 'org.fdroid.fdroid'
        paths = [
            os.path.join('source-files', 'fdroid', 'fdroidclient', 'AndroidManifest.xml'),
            os.path.join('source-files', 'fdroid', 'fdroidclient', 'build.gradle'),
        ]
        for path in paths:
            self.assertTrue(os.path.isfile(path))
        self.assertEqual(('0.94-test', '940', 'org.fdroid.fdroid'),
                         fdroidserver.common.parse_androidmanifests(paths, app))

    def test_parse_androidmanifests_ignore(self):
        app = fdroidserver.metadata.App()
        app.id = 'org.fdroid.fdroid'
        app.UpdateCheckIgnore = '-test'
        paths = [
            os.path.join('source-files', 'fdroid', 'fdroidclient', 'AndroidManifest.xml'),
            os.path.join('source-files', 'fdroid', 'fdroidclient', 'build.gradle'),
        ]
        for path in paths:
            self.assertTrue(os.path.isfile(path))
        self.assertEqual(('Ignore', None, 'org.fdroid.fdroid'),
                         fdroidserver.common.parse_androidmanifests(paths, app))

    def test_parse_androidmanifests_with_flavor(self):
        app = fdroidserver.metadata.App()
        build = fdroidserver.metadata.Build()
        build.gradle = ['devVersion']
        app.builds = [build]
        app.id = 'org.fdroid.fdroid.dev'
        paths = [
            os.path.join('source-files', 'fdroid', 'fdroidclient', 'AndroidManifest.xml'),
            os.path.join('source-files', 'fdroid', 'fdroidclient', 'build.gradle'),
        ]
        for path in paths:
            self.assertTrue(os.path.isfile(path))
        self.assertEqual(('0.95-dev', '949', 'org.fdroid.fdroid.dev'),
                         fdroidserver.common.parse_androidmanifests(paths, app))

        app = fdroidserver.metadata.App()
        build = fdroidserver.metadata.Build()
        build.gradle = ['free']
        app.builds = [build]
        app.id = 'eu.siacs.conversations'
        paths = [
            os.path.join('source-files', 'eu.siacs.conversations', 'build.gradle'),
        ]
        for path in paths:
            self.assertTrue(os.path.isfile(path))
        self.assertEqual(('1.23.1', '245', 'eu.siacs.conversations'),
                         fdroidserver.common.parse_androidmanifests(paths, app))

        app = fdroidserver.metadata.App()
        build = fdroidserver.metadata.Build()
        build.gradle = ['generic']
        app.builds = [build]
        app.id = 'com.nextcloud.client'
        paths = [
            os.path.join('source-files', 'com.nextcloud.client', 'build.gradle'),
        ]
        for path in paths:
            self.assertTrue(os.path.isfile(path))
        self.assertEqual(('2.0.0', '20000099', 'com.nextcloud.client'),
                         fdroidserver.common.parse_androidmanifests(paths, app))

        app = fdroidserver.metadata.App()
        build = fdroidserver.metadata.Build()
        build.gradle = ['versionDev']
        app.builds = [build]
        app.id = 'com.nextcloud.android.beta'
        paths = [
            os.path.join('source-files', 'com.nextcloud.client', 'build.gradle'),
        ]
        for path in paths:
            self.assertTrue(os.path.isfile(path))
        self.assertEqual(('20171223', '20171223', 'com.nextcloud.android.beta'),
                         fdroidserver.common.parse_androidmanifests(paths, app))

        app = fdroidserver.metadata.App()
        build = fdroidserver.metadata.Build()
        build.gradle = ['standard']
        app.builds = [build]
        app.id = 'at.bitfire.davdroid'
        paths = [
            os.path.join('source-files', 'at.bitfire.davdroid', 'build.gradle'),
        ]
        for path in paths:
            self.assertTrue(os.path.isfile(path))
        self.assertEqual(('1.9.8.1-ose', '197', 'at.bitfire.davdroid'),
                         fdroidserver.common.parse_androidmanifests(paths, app))

        app = fdroidserver.metadata.App()
        build = fdroidserver.metadata.Build()
        build.gradle = ['libre']
        app.builds = [build]
        app.id = 'com.kunzisoft.fdroidtest.applicationidsuffix.libre'
        paths = [
            os.path.join('source-files', 'com.kunzisoft.testcase', 'build.gradle'),
        ]
        for path in paths:
            self.assertTrue(os.path.isfile(path))
        self.assertEqual(('1.0-libre', '1', 'com.kunzisoft.fdroidtest.applicationidsuffix.libre'),
                         fdroidserver.common.parse_androidmanifests(paths, app))

        app = fdroidserver.metadata.App()
        build = fdroidserver.metadata.Build()
        build.gradle = ['pro']
        app.builds = [build]
        app.id = 'com.kunzisoft.fdroidtest.applicationidsuffix.pro'
        paths = [
            os.path.join('source-files', 'com.kunzisoft.testcase', 'build.gradle'),
        ]
        for path in paths:
            self.assertTrue(os.path.isfile(path))
        self.assertEqual(('20180430-pro', '20180430', 'com.kunzisoft.fdroidtest.applicationidsuffix.pro'),
                         fdroidserver.common.parse_androidmanifests(paths, app))

        app = fdroidserver.metadata.App()
        build = fdroidserver.metadata.Build()
        build.gradle = ['free']
        app.builds = [build]
        app.id = 'com.kunzisoft.fdroidtest.applicationidsuffix'
        paths = [
            os.path.join('source-files', 'com.kunzisoft.testcase', 'build.gradle'),
        ]
        for path in paths:
            self.assertTrue(os.path.isfile(path))
        self.assertEqual(('1.0-free', '1', 'com.kunzisoft.fdroidtest.applicationidsuffix'),
                         fdroidserver.common.parse_androidmanifests(paths, app))

    def test_remove_signing_keys(self):
        testdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, dir=self.tmpdir)
        print(testdir)
        shutil.copytree(os.path.join(self.basedir, 'source-files'),
                        os.path.join(testdir, 'source-files'))
        os.chdir(testdir)
        with_signingConfigs = [
            'source-files/com.seafile.seadroid2/app/build.gradle',
            'source-files/eu.siacs.conversations/build.gradle',
            'source-files/info.guardianproject.ripple/build.gradle',
            'source-files/open-keychain/open-keychain/build.gradle',
            'source-files/open-keychain/open-keychain/OpenKeychain/build.gradle',
            'source-files/osmandapp/osmand/build.gradle',
            'source-files/ut.ewh.audiometrytest/app/build.gradle',
        ]
        for f in with_signingConfigs:
            build_dir = os.path.join(*f.split(os.sep)[:2])
            if not os.path.isdir(build_dir):
                continue
            fdroidserver.common.remove_signing_keys(build_dir)
            fromfile = os.path.join(self.basedir, f)
            with open(f) as fp:
                content = fp.read()
            if 'signingConfig' in content:
                with open(f) as fp:
                    b = fp.readlines()
                with open(fromfile) as fp:
                    a = fp.readlines()
                diff = difflib.unified_diff(a, b, fromfile, f)
                sys.stdout.writelines(diff)
                self.assertFalse(True)
        do_not_modify = [
            'source-files/Zillode/syncthing-silk/build.gradle',
            'source-files/at.bitfire.davdroid/build.gradle',
            'source-files/com.kunzisoft.testcase/build.gradle',
            'source-files/com.nextcloud.client/build.gradle',
            'source-files/fdroid/fdroidclient/build.gradle',
            'source-files/firebase-suspect/app/build.gradle',
            'source-files/firebase-suspect/build.gradle',
            'source-files/firebase-whitelisted/app/build.gradle',
            'source-files/firebase-whitelisted/build.gradle',
            'source-files/org.mozilla.rocket/app/build.gradle',
            'source-files/realm/react-native/android/build.gradle',
            'triple-t-2/build/org.piwigo.android/app/build.gradle',
        ]
        for f in do_not_modify:
            build_dir = os.path.join(*f.split(os.sep)[:2])
            if not os.path.isdir(build_dir):
                continue
            fdroidserver.common.remove_signing_keys(build_dir)
            fromfile = os.path.join(self.basedir, f)
            with open(fromfile) as fp:
                a = fp.readlines()
            with open(f) as fp:
                b = fp.readlines()
            diff = list(difflib.unified_diff(a, b, fromfile, f))
            self.assertEqual(0, len(diff), 'This file should not have been modified:\n' + ''.join(diff))

    def test_calculate_math_string(self):
        self.assertEqual(1234,
                         fdroidserver.common.calculate_math_string('1234'))
        self.assertEqual((1 + 1) * 2,
                         fdroidserver.common.calculate_math_string('(1 + 1) * 2'))
        self.assertEqual((1 - 1) * 2 + 3 * 1 - 1,
                         fdroidserver.common.calculate_math_string('(1 - 1) * 2 + 3 * 1 - 1'))
        self.assertEqual(0 - 12345,
                         fdroidserver.common.calculate_math_string('0 - 12345'))
        self.assertEqual(0xffff,
                         fdroidserver.common.calculate_math_string('0xffff'))
        self.assertEqual(0xcafe * 123,
                         fdroidserver.common.calculate_math_string('0xcafe * 123'))
        self.assertEqual(-1,
                         fdroidserver.common.calculate_math_string('-1'))
        with self.assertRaises(SyntaxError):
            fdroidserver.common.calculate_math_string('__import__("urllib")')
        with self.assertRaises(SyntaxError):
            fdroidserver.common.calculate_math_string('self')
        with self.assertRaises(SyntaxError):
            fdroidserver.common.calculate_math_string('Ox9()')
        with self.assertRaises(SyntaxError):
            fdroidserver.common.calculate_math_string('1+1; print(1)')
        with self.assertRaises(SyntaxError):
            fdroidserver.common.calculate_math_string('1-1 # no comment')

    def test_deploy_build_log_with_rsync_with_id_file(self):

        mocklogcontent = bytes(textwrap.dedent("""\
            build started
            building...
            build completed
            profit!"""), 'utf-8')

        fdroidserver.common.options = mock.Mock()
        fdroidserver.common.options.verbose = False
        fdroidserver.common.options.quiet = False
        fdroidserver.common.config = {}
        fdroidserver.common.config['serverwebroot'] = [
            'example.com:/var/www/fdroid/',
            'example.com:/var/www/fbot/']
        fdroidserver.common.config['deploy_process_logs'] = True
        fdroidserver.common.config['identity_file'] = 'ssh/id_rsa'

        assert_subprocess_call_iteration = 0

        def assert_subprocess_call(cmd):
            nonlocal assert_subprocess_call_iteration
            logging.debug(cmd)
            if assert_subprocess_call_iteration == 0:
                self.assertListEqual(['rsync',
                                      '--archive',
                                      '--delete-after',
                                      '--safe-links',
                                      '-e',
                                      'ssh -oBatchMode=yes -oIdentitiesOnly=yes -i ssh/id_rsa',
                                      cmd[6],
                                      'example.com:/var/www/fdroid/repo/'],
                                     cmd)
                self.assertTrue(cmd[6].endswith('/com.example.app_4711.log.gz'))
                with gzip.open(cmd[6], 'r') as f:
                    self.assertTrue(f.read(), mocklogcontent)
            elif assert_subprocess_call_iteration == 1:
                self.assertListEqual(['rsync',
                                      '--archive',
                                      '--delete-after',
                                      '--safe-links',
                                      '-e',
                                      'ssh -oBatchMode=yes -oIdentitiesOnly=yes -i ssh/id_rsa',
                                      cmd[6],
                                      'example.com:/var/www/fbot/repo/'],
                                     cmd)
                self.assertTrue(cmd[6].endswith('/com.example.app_4711.log.gz'))
                with gzip.open(cmd[6], 'r') as f:
                    self.assertTrue(f.read(), mocklogcontent)
            else:
                self.fail('unexpected subprocess.call invocation ({})'
                          .format(assert_subprocess_call_iteration))
            assert_subprocess_call_iteration += 1
            return 0

        with mock.patch('subprocess.call',
                        side_effect=assert_subprocess_call):
            with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir):
                fdroidserver.common.deploy_build_log_with_rsync(
                    'com.example.app', '4711', mocklogcontent)

                expected_log_path = os.path.join(tmpdir, 'repo', 'com.example.app_4711.log.gz')
                self.assertTrue(os.path.isfile(expected_log_path))
                with gzip.open(expected_log_path, 'r') as f:
                    self.assertEqual(f.read(), mocklogcontent)

    def test_string_is_integer(self):
        self.assertTrue(fdroidserver.common.string_is_integer('0x10'))
        self.assertTrue(fdroidserver.common.string_is_integer('010'))
        self.assertTrue(fdroidserver.common.string_is_integer('123'))
        self.assertFalse(fdroidserver.common.string_is_integer('0xgg'))
        self.assertFalse(fdroidserver.common.string_is_integer('01g'))
        self.assertFalse(fdroidserver.common.string_is_integer('o123'))

    def test_version_code_string_to_int(self):
        self.assertEqual(16, fdroidserver.common.version_code_string_to_int('0x10'))
        self.assertEqual(198712389, fdroidserver.common.version_code_string_to_int('198712389'))
        self.assertEqual(8, fdroidserver.common.version_code_string_to_int('0o10'))
        self.assertEqual(10, fdroidserver.common.version_code_string_to_int('010'))
        self.assertEqual(123, fdroidserver.common.version_code_string_to_int('0000123'))
        self.assertEqual(-42, fdroidserver.common.version_code_string_to_int('-42'))


if __name__ == "__main__":
    os.chdir(os.path.dirname(__file__))

    parser = optparse.OptionParser()
    parser.add_option("-v", "--verbose", action="store_true", default=False,
                      help="Spew out even more information than normal")
    (fdroidserver.common.options, args) = parser.parse_args(['--verbose'])

    newSuite = unittest.TestSuite()
    newSuite.addTest(unittest.makeSuite(CommonTest))
    unittest.main(failfast=False)
